﻿/*
 * This code is part of cav94mat libraries.
 * (c) 2013, cav94mat - http://www.cav94mat.com/
 *
 *  -- Distributed under terms of the Simplified BSD License --
 * 
 *  Redistribution and use in source and binary forms, with or without modification, are permitted
 *  provided that the following conditions are met:
 *
 *  - Redistributions of source code must retain the above copyright notice, this list of conditions
 *    and the following disclaimer.
 *  - Redistributions in binary form must reproduce the above copyright notice, this list of
 *    conditions and the following disclaimer in the documentation and/or other materials provided
 *    with the distribution.
 * 
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
 *  IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 *  AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
 *  OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
 *  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 *  OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 *  POSSIBILITY OF SUCH DAMAGE.
 * 
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Collections.ObjectModel;
using System.Reflection;
using System.Resources;
namespace cav94mat.Manuals
{
    public class Manual
    {
        #region 1. Properties
            public string HeaderTitle { get; set; }
            public string HeaderSubtitle { get; set; }
            static readonly Version ManualInterpreterVersion = new Version("1.0");
        #endregion
        #region 2. Style
            public ConsoleColor CurrentBodyBackground = DefaultBodyBackground;
            public ConsoleColor CurrentBodyForeground = DefaultBodyForeground;
            public ConsoleColor CurrentBodyBoldForeground = DefaultBodyBoldForeground;
            public ConsoleColor CurrentHeaderBackground = DefaultHeaderBackground;
            public ConsoleColor CurrentHeaderForeground = DefaultHeaderForeground;
            public ConsoleColor CurrentSectionTitleBackground = DefaultSectionTitleBackground;
            public ConsoleColor CurrentSectionTitleForeground = DefaultSectionTitleForeground;
            public ConsoleColor CurrentFooterBackground = DefaultFooterBackground;
            public ConsoleColor CurrentFooterForeground = DefaultFooterForeground;
        #endregion
        #region 3. Default style
            public static ConsoleColor DefaultBodyBackground = ConsoleColor.Black;
            public static ConsoleColor DefaultBodyForeground = ConsoleColor.Gray;
            public static ConsoleColor DefaultBodyBoldForeground = ConsoleColor.White;
            public static ConsoleColor DefaultHeaderBackground = ConsoleColor.Gray;
            public static ConsoleColor DefaultHeaderForeground = ConsoleColor.Black;
            public static ConsoleColor DefaultSectionTitleBackground = ConsoleColor.DarkGray;
            public static ConsoleColor DefaultSectionTitleForeground = ConsoleColor.Black;
            public static ConsoleColor DefaultFooterBackground = ConsoleColor.Gray;
            public static ConsoleColor DefaultFooterForeground = ConsoleColor.Black;
        #endregion
        #region 4. Internal vars
            private string _source;
            private uint _clientY = 0;
            private uint _initialY = 0;
            private uint _initialBW = 0;
            private uint _initialWW = 0;
            private uint _initialBH = 0;
            private uint _initialWH = 0;
            private bool _initialCur = false;
            private ConsoleColor _initialCF;
            private ConsoleColor _initialCB;
            private uint _currentWH = 0;
            private uint _currentWW = 0;
            //Private _currentRY As UInteger = 0
            //Private _currentRX As UInteger = 0
            private uint _currentDocW = 0;
            private uint _currentDocH = 0;
            private uint _currentDocX = 0;
            private uint _currentDocY = 0;
        #endregion
        #region 5. Interpreter methods / initializers
            private Manual(string source)
            {
                _source = "";
                _currentDocW = 0;
                _currentDocH = 0;
                _currentWH = (uint)Console.WindowHeight;
                _currentWW = (uint)Console.WindowWidth;
                Dictionary<string, object> _parserData = new Dictionary<string, object>();
                int _row = -1;
                _parserData.Add("mode", "natural");
                _parserData.Add("highlight", "none");
                // Pre-parse
                foreach (string i in source.Split("\n".ToCharArray(), StringSplitOptions.None))
                {
                    _row += 1;
                    try
                    {
                        if (i.StartsWith("#"))
                        {
                            // Macro line
                            string l = i.Substring(1);
                            string cmd = null;
                            int sectionRecurse = 0;
                            Collection<string> args = null;
                            Collection<string> flags = null;
                            Dictionary<string, string> props = null;
                            ConsoleUtils.ExplodeCmdArgs(l, ref args, ref flags, ref props);
                            cmd = args[0].ToLower().Trim();
                            switch (cmd)
                            {
                                case "manual": case "version":
                                    Version v = new Version(args[1]);
                                    if (v > ManualInterpreterVersion)
                                        throw new NotSupportedException("The manual interpreter bundled with this assembly is outdated.");
                                    break;
                                case "head": case "title":
                                    this.HeaderTitle = ResolveVariables(args[1]);
                                    break;
                                case "headsub": case "subhead": case "titlesub": case "subtitle":
                                    this.HeaderSubtitle = ResolveVariables(args[1]);
                                    break;
                                case "width":
                                    _currentWW = uint.Parse(args[1]);
                                    break;
                                case "height":
                                    _currentWH = uint.Parse(args[1]);
                                    break;
                                case "section": case "section-begin":
                                    sectionRecurse = Math.Min(int.MaxValue, sectionRecurse + 1);
                                    _source += "\n" + Space(sectionRecurse) + " @@__SECTION; " + args[1] + " @@/__SECTION;" + "\n\n";
                                    _currentDocH += 3;
                                    break;
                                case "/section": case "section-end":
                                    sectionRecurse = Math.Min(0, sectionRecurse - 1);
                                    break;
                                case "highlight": case "highlight-begin": case "/highlight": case "highlight-end":
                                    if (args[1] == "none" || cmd== "/highlight" || cmd == "highlight-end")
                                        args[1] = "";
                                    _parserData["highlight"] = args[1];
                                    break;
                                case "table-begin": case "table":
                                    if (_parserData.ContainsKey("table"))
                                    {
                                        throw new SyntaxErrorException("Line " + _row + ": Keyword '#table-end' expected.", null);
                                    }
                                    _parserData.Add("table", true);
                                    _parserData.Add("table-indent", 1);
                                    _parserData.Add("table-separator", '|');

                                    if (props.ContainsKey("indentation"))
                                        _parserData["table-indent"] = Convert.ToInt32(props["indentation"]);
                                    if (props.ContainsKey("separator"))
                                        _parserData["table-separator"] = Convert.ToChar(props["separator"]);

                                    if (args.Count == 0)
                                        throw new SyntaxErrorException("Line " + _row + ": No columns declared for table.", null);
                                    _source += "\n" + Space(int.Parse(_parserData["table-indent"].ToString()));
                                    uint fsz = 0;
                                    for (int k = 0; k <= args.Count - 2; k++)
                                    {
                                        string a = args[k + 1].Trim();
                                        try
                                        {
                                            string col = a.Split(":".ToCharArray(), 2)[0];
                                            string sz = a.Split(":".ToCharArray(), 2)[1];
                                            if (sz == "*")
                                                sz = col.Length.ToString();
                                            if (!(Convert.ToInt32(sz.TrimEnd("%".ToCharArray())) == Convert.ToInt32(sz.TrimEnd("%".ToCharArray()))))
                                                throw new SyntaxErrorException("Line " + _row + ": Invalid table column size specification (" + i + "): '" + sz + "' is not a valid size.", null);
                                            if (sz.EndsWith("%"))
                                            {
                                                sz = Convert.ToInt32(sz.TrimEnd("%".ToCharArray())).ToString();
                                                sz = Convert.ToInt32((_currentWW - 3) * (uint.Parse(sz) / 100)).ToString();
                                            }
                                            fsz += uint.Parse(sz);
                                            uint len = 0;
                                            ResolveVariables(col, len);
                                            if (int.Parse(sz) < len)
                                                col = col.Substring(0, int.Parse(sz));

                                            _source += "@@__SECTION;" + col + Space((int)(int.Parse(sz) - len)) + "@@/__SECTION; ";
                                            _parserData["table"] = k + 1;
                                            _parserData.Add("table[" + k + "]", sz);
                                        }
                                        catch (SyntaxErrorException sx)
                                        {
                                            throw sx;
                                        }
                                        catch (Exception ex)
                                        {
                                            if (args.Count == 0)
                                                throw new SyntaxErrorException("Line " + _row + ": Bad table column declaration (" + i + "): '" + a + "'.", null);
                                        }
                                    }

                                    if (fsz > _currentDocW)
                                        _currentDocW = fsz;
                                    //Refresh record
                                    _source = _source.TrimEnd(' ') + "\n";
                                    _currentDocH += 1;
                                    break;
                                case "table-end": case "/table":
                                    if (!_parserData.ContainsKey("table"))
                                    {
                                        throw new SyntaxErrorException("Line " + _row + ": Keyword '#table-begin' expected.", null);
                                    }
                                    for (int k = 0; k <= uint.Parse(_parserData["table"].ToString()) - 1; k++)
                                    {
                                        _parserData.Remove("table[" + k + "]");
                                    }

                                    _parserData.Remove("table-indent");
                                    _parserData.Remove("table-separator");
                                    _parserData.Remove("table");
                                    _source += "\n";
                                    _currentDocH += 1;
                                    break;
                                default:
                                    break;
                            }
                        }
                        else if (i.StartsWith(";"))
                        {
                            // is a comment.
                        }
                        else
                        {
                            string l = i.Replace("\n", "").Replace("\r", "");
                            if (_parserData.ContainsKey("table"))
                            {
                                // Reading table rows
                                string[] cols = l.Split(_parserData["table-separator"].ToString().ToCharArray());
                                l = Space(int.Parse(_parserData["table-indent"].ToString()));
                                for (int k = 0; k <= cols.Length - 1; k++)
                                {
                                    uint len = 0;
                                    string c = ResolveVariables(cols[k], len);
                                    if (len > uint.Parse(_parserData["table[" + i + "]"].ToString()))
                                        c = c.Substring(0, int.Parse(_parserData["table[" + i + "]"].ToString()) - 1) + " ";
                                    l += c + Space(int.Parse(_parserData["table[" + i + "]"].ToString()) - c.Length);
                                }
                            }
                            else
                            {
                                uint len = 0;
                                ResolveVariables(l, len);
                                if (len > _currentDocW)
                                    _currentDocW = len;
                                //Refresh record
                            }
                            _source += ParseWithHighlight(l, ref _parserData) + "\n";
                            _currentDocH += 1;
                        }
                    }
                    catch (SyntaxErrorException ex)
                    {
                        throw ex; 
                    }
    #if !DEBUG
                    catch (Exception ex)
                    {
                        throw new SyntaxErrorException("Line " + _row + ": Invalid row or bad command syntax. The manual source might be damaged.", null);
                    }
    #endif
                }
                _currentDocH += 2;
            }
            public static Manual GetManual(string source)
            {
                return new Manual(source);
            }
            public static Manual GetManualFromRes(string resourceName)
            {
                return new Manual(new ResourceManager(Assembly.GetCallingAssembly().GetManifestResourceNames()[0].Replace(".Resources.resources", ".Resources"), Assembly.GetCallingAssembly()).GetString(resourceName));
                //return GetManual(new System.IO.StreamReader(Assembly.GetCallingAssembly().GetManifestResourceStream( Assembly.GetCallingAssembly().GetManifestResourceNames()[0] + "." +  resourceName)).ReadToEnd());
            }
            protected virtual string ResolveVariables(string input, uint len = uint.MaxValue)
            {
                string functionReturnValue = null;
                functionReturnValue = input;

                // Variables replacement
                // 1. Manifest
                FileVersionInfo myapp = FileVersionInfo.GetVersionInfo(Assembly.GetCallingAssembly().Location);
                functionReturnValue = functionReturnValue.Replace("@@MYEXE;", Process.GetCurrentProcess().ProcessName);
                functionReturnValue = functionReturnValue.Replace("@@MYVERSION;", myapp.ProductMajorPart + "." + myapp.ProductMinorPart);
                functionReturnValue = functionReturnValue.Replace("@@MYVERSIONFULL;", myapp.ProductVersion);
                functionReturnValue = functionReturnValue.Replace("@@MYCOPYRIGHT;", myapp.LegalCopyright);
                functionReturnValue = functionReturnValue.Replace("@@MYAPPTITLE;", myapp.ProductName);
                functionReturnValue = functionReturnValue.Replace("@@MYAUTHOR;", myapp.CompanyName);
                // 2. Special chars
                foreach (string col in Enum.GetNames(typeof(ConsoleColor)))
                {
                    functionReturnValue = functionReturnValue.Replace("@@SETBACK(" + col + ");", (char)27 + "B" + Hex(Array.IndexOf(Enum.GetNames(typeof(ConsoleColor)), col)));
                    functionReturnValue = functionReturnValue.Replace("@@SETFORE(" + col + ");", (char)27 + "F" + Hex(Array.IndexOf(Enum.GetNames(typeof(ConsoleColor)), col)));
                }
                functionReturnValue = functionReturnValue.Replace("@@__SECTION;", (char)27 + "BS" + (char)27 + "FS");
                functionReturnValue = functionReturnValue.Replace("@@/__SECTION;", (char)27 + "EF" + (char)27 + "EB");

                functionReturnValue = functionReturnValue.Replace("@@/SETFORE;", (char)27 + "EF");
                functionReturnValue = functionReturnValue.Replace("@@/SETBACK;", (char)27 + "EB");
                functionReturnValue = functionReturnValue.Replace("@@BOLD;", (char)27 + "G1");
                functionReturnValue = functionReturnValue.Replace("@@/BOLD;", (char)27 + "G0");
                functionReturnValue = functionReturnValue.Replace(Environment.NewLine, "").Replace("\r", "").Replace("\n", "");
                functionReturnValue = functionReturnValue.Replace("\t", Space(4));
                if (len != uint.MaxValue)
                {
                    len = 0;
                    for (int i = 0; i < functionReturnValue.Length; i++)
                    {
                        if ((int)(functionReturnValue[i]) != 27)
                            len += 1;
                        else
                            i += 3;
                    }
                }
                return functionReturnValue;
            }

            private string Space(int p)
            {
                string output = "";
                for (uint i = 0; i < p; i++) output += " ";
                return output;
            }
            protected virtual string ParseWithHighlight(string input, ref Dictionary<string, object> parser)
            {
                switch (parser["highlight"].ToString())
                {
                    case "syntax":
                        input = input.Replace("[", (char)27 + "F" + Hex((int)ConsoleColor.DarkGray) + "[");
                        input = input.Replace("]", "]" + (char)27 + "EF");
                        input = input.Replace("<", (char)27 + "F" + Hex((int)ConsoleColor.Green) + "<" + (char)27 + "F" + Hex((int)ConsoleColor.White));
                        input = input.Replace(">", (char)27 + "EF" + ">" + (char)27 + "EF");
                        input = input.Replace("{", (char)27 + "F" + Hex((int)ConsoleColor.Cyan) + "{" + (char)27 + "EF");
                        input = input.Replace("}", (char)27 + "F" + Hex((int)ConsoleColor.Cyan) + "}" + (char)27 + "EF");
                        input = input.Replace("|", (char)27 + "F" + Hex((int)ConsoleColor.White) + "|" + (char)27 + "EF");
                        return input;
                    case "null":
                        return "";
                    default:
                        return input;
                }
            }

            private string Hex(int dec)
            {
                string output = dec.ToString("X");
                output = output[output.Length - 1].ToString();
                return output;
            }
        #endregion
        #region 6. Console rendering
            public void BeginRender()
            {
                // ERROR: Not supported in C#: OnErrorStatement

                // Buffer backup
                _initialY = (uint)Console.CursorTop;
                _initialBH = (uint)Console.BufferHeight;
                _initialBW = (uint)Console.BufferWidth;
                _initialWH = (uint)Console.WindowHeight;
                _initialWW = (uint)Console.WindowWidth;
                _initialCur = Console.CursorVisible;
                _initialCF = Console.ForegroundColor;
                _initialCB = Console.BackgroundColor;

                // Buffer positioning/resizing
                if (Console.BufferHeight < _initialY + Console.WindowHeight * 2)
                    Console.BufferHeight += Console.WindowHeight * 2;
                Console.WindowWidth = (int)_currentWW;
                Console.WindowHeight = (int)_currentWH;
                Console.BufferWidth = Console.WindowWidth + 1;
                //_currentBH = BufferHeight
                Console.CursorVisible = false;
                Console.CursorTop = Console.BufferHeight - 1;
                Console.CursorTop = Console.BufferHeight - Console.WindowHeight;
                //Title = "MANUAL - " & Me.HeaderTitle
                _clientY = (uint)(Console.BufferHeight - Console.WindowHeight);

                RenderClearArea(false);
                // Header drawing
                RenderHeader();
                // Footer drawings
                RenderFooter("");
                // Body drawing
                RenderBody();
                bool running = true;
                while (running)
                {
                    switch (Console.ReadKey(true).Key)
                    {
                        case ConsoleKey.UpArrow:
                            if (_currentDocY <= 0)
                                continue;
                            _currentDocY -= 1;
                            break;
                        case ConsoleKey.DownArrow:
                        case ConsoleKey.Enter:
                            if (_currentDocY > (_currentDocH - Console.WindowHeight))
                                continue;
                            _currentDocY += 1;
                            break;
                        case ConsoleKey.PageUp:
                            if (_currentDocY <= 0)
                                continue;
                            _currentDocY -= Math.Min(10, _currentDocY);
                            break;
                        case ConsoleKey.PageDown:
                        case ConsoleKey.Spacebar:
                            if (_currentDocY > (_currentDocH - Console.WindowHeight))
                                continue;
                            _currentDocY += (uint)Math.Min(10, (_currentDocH - Console.WindowHeight) + 1 - _currentDocY);
                            break;
                        case ConsoleKey.LeftArrow:
                            if (_currentDocX <= 0)
                                continue;
                            _currentDocX -= 1;
                            break;
                        case ConsoleKey.RightArrow:
                            if (_currentDocX > (_currentDocW - Console.WindowWidth))
                                continue;
                            _currentDocX += 1;
                            break;
                        case ConsoleKey.Home:
                            _currentDocY = 0;
                            break;
                        case ConsoleKey.End:
                            _currentDocY = (uint)(_currentDocH - Console.WindowHeight) + 1;
                            break;
                        case ConsoleKey.Escape:
                        case ConsoleKey.Q:
                             // TODO: might not be correct. Was : Exit Do
                            running = false;
                            break;
                    }
                    //RenderFooter("")
                    RenderBody();
                };
                StopRender();
            }
            public void StopRender()
            {
                Console.CursorTop = (int)_clientY - 1;
                Console.ForegroundColor = _initialCF;
                Console.BackgroundColor = _initialCB;
                RenderClearArea(false);
                Console.CursorTop = Math.Max(Convert.ToInt32(_initialY) - Convert.ToInt32(_initialWH), 0);
                Console.CursorTop = (int)_initialY;
                Console.CursorLeft = 0;
                Console.WindowHeight = (int)_initialWH;
                Console.WindowWidth = (int)_initialWW;
                Console.BufferHeight = (int)_initialBH;
                Console.BufferWidth = (int)_initialBW;
                Console.CursorVisible = _initialCur;
            }

            protected virtual void RenderHeader()
            {
                Console.BackgroundColor = this.CurrentHeaderBackground;
                Console.ForegroundColor = this.CurrentHeaderForeground;

                RenderClearRow(this.CurrentHeaderBackground, 0);
                RenderAlignedText(this.HeaderTitle, 0);
                Console.CursorTop += 1;
                Console.BackgroundColor = this.CurrentSectionTitleBackground;
                Console.ForegroundColor = this.CurrentSectionTitleForeground;
                RenderClearRow(this.CurrentSectionTitleBackground, 1);
                RenderAlignedText(this.HeaderSubtitle, 0);
            }

            protected virtual void RenderBody(uint xOffset, uint yOffset)
            {
                Console.CursorTop = (int)_clientY + 2;
                Console.CursorLeft = 0;
                Console.BackgroundColor = this.CurrentBodyBackground;
                Console.ForegroundColor = this.CurrentBodyForeground;
                Console.Write(Space(Console.BufferWidth * (Console.WindowHeight - 3)));
                Console.CursorTop = (int)_clientY + 2;
                Console.CursorLeft = 0;
                // Parse
                int count = -1;
                foreach (string j in _source.Split("\n".ToCharArray()))
                {
                    if (j.StartsWith("#"))
                    {
                        // Macro line
                        string l = j.Substring(1);
                        string cmd = null;
                        Collection<string> args = null;
                        Collection<string> flags = null;
                        Dictionary<string, string> props = null;
                        ConsoleUtils.ExplodeCmdArgs(l, ref args, ref flags, ref props);
                        cmd = args[0].ToLower();
                        switch (cmd)
                        {
                            default:
                                break;
                        }
                        //is a comment
                    }
                    else if (j.StartsWith(";"))
                    {
                    }
                    else
                    {
                        count += 1;
                        if (count < yOffset)
                            continue;
                        if (Console.CursorTop > Console.BufferHeight - 2)
                            return;
                        RenderText(ResolveVariables(j).TrimEnd("\n".ToCharArray()) + "\n", (int)xOffset, 0);
                    }
                }
                //Console.Title = xOffset + ":" + yOffset; // Debug
            }
            protected virtual void RenderText(string text, int begin = -1, int end = 0)
            {
                int count = -1;
                Collection<ConsoleColor> preFore = new Collection<ConsoleColor>();
                Collection<ConsoleColor> preBack = new Collection<ConsoleColor>();
                if (end == 0)
                    end = int.MaxValue;
                for (int i = 0; i <= text.Length - 1; i++)
                {
                    if ((int)(text[i]) == 27)
                    {
                        switch (text[i + 1])
                        {
                            case 'F':
                                preFore.Insert(0, Console.ForegroundColor);
                                if (text[i + 2] == 'S')
                                {
                                    Console.ForegroundColor = this.CurrentSectionTitleForeground;
                                }
                                else if (text[i + 2] == 'H')
                                {
                                    Console.ForegroundColor = this.CurrentHeaderForeground;
                                }
                                else if (text[i + 2] == 'T')
                                {
                                    Console.ForegroundColor = this.CurrentFooterForeground;
                                }
                                else if (text[i + 2] == 'Y')
                                {
                                    Console.ForegroundColor = this.CurrentBodyForeground;
                                }
                                else if (text[i + 2] == 'G')
                                {
                                    Console.ForegroundColor = this.CurrentBodyBoldForeground;
                                }
                                else
                                {
                                    Console.ForegroundColor = (ConsoleColor)Convert.ToInt32(text[i + 2].ToString(), (int)16);
                                }
                                break;
                            case 'B':
                                preBack.Insert(0, Console.BackgroundColor);
                                if (text[i + 2] == 'S')
                                {
                                    Console.BackgroundColor = this.CurrentSectionTitleBackground;
                                }
                                else if (text[i + 2] == 'H')
                                {
                                    Console.BackgroundColor = this.CurrentHeaderBackground;
                                }
                                else if (text[i + 2] == 'T')
                                {
                                    Console.BackgroundColor = this.CurrentFooterBackground;
                                }
                                else if (text[i + 2] == 'Y')
                                {
                                    Console.BackgroundColor = this.CurrentBodyBackground;
                                }
                                else
                                {
                                    Console.BackgroundColor = (ConsoleColor)Convert.ToInt32("0x" + text[i + 2]);
                                }
                                break;
                            case 'E':
                                switch (text[i + 2])
                                {
                                    case 'F':
                                        if (preFore.Count == 0) { i += 2; continue; }
                                        Console.ForegroundColor = preFore[0];
                                        preFore.RemoveAt(0);
                                        break;
                                    case 'B':
                                        if (preBack.Count == 0) { i += 2; continue; }
                                        Console.BackgroundColor = preBack[0];
                                        preBack.RemoveAt(0);
                                        break;
                                }
                                break;
                            case 'G':
                                if (text[i + 2] == '1')
                                {
                                    Console.ForegroundColor = CurrentBodyBoldForeground;
                                }
                                else
                                {
                                    Console.ForegroundColor = CurrentBodyForeground;
                                }
                                break;
                        }
                        i += 2;
                    }
                    else if (text[i] == "\r"[0] | text[i] == '\n')
                    {
                        continue;
                    }
                    else
                    {
                        count += 1;
                        if (count < begin | count > end)
                            continue;
                        if (Console.CursorLeft < Console.WindowWidth - 1)
                        {
                            Console.Write(text[i]);
                            //If CursorLeft >= WindowWidth - 1 Then Write(text(i + 1)) : Exit For
                        }
                        else
                        {
                            continue;
                        }
                    }
                }
                if (text.EndsWith("\n"))
                    Console.WriteLine();
            }
            protected virtual void RenderFooter(string text)
            {
                Console.BackgroundColor = this.CurrentFooterBackground;
                Console.ForegroundColor = this.CurrentFooterForeground;

                //Dim grayed As String = Chr(27) & "F" & Hex(ConsoleColor.DarkGray)
                //Dim norm As String = Chr(27) & "EF"
                //If text = "" Then text = IIf(_currentDocY = 0, grayed, "") & "▲" & norm & _
                //    "/" & IIf(_currentDocY > _currentDocH - WindowHeight + 1, grayed, "") & "▼" & norm & _
                //    "/►/◄ : Scroll | pag▲/pag▼ : Page scroll | home/end : Top/Bottom | ESC: Quit"
                if (string.IsNullOrEmpty(text))
                    text = "▲/▼/►/◄ : Scroll | pag▲/pag▼ : Page scroll | home/end : Top/Bottom | ESC: Quit";
                //BufferHeight = _currentBH

                Console.CursorTop = (int)_clientY + Console.WindowHeight - 1;
                RenderClearRow(this.CurrentFooterBackground, (uint)Console.WindowHeight - 1);
                RenderText(text, -1, Console.WindowWidth - 1);
                Console.CursorTop = (int)_clientY - 1;
                Console.CursorLeft = 0;
                //BufferHeight -= 1
            }
            protected virtual void RenderAlignedText(string text, sbyte alignment)
            {
                int x0 = Console.CursorLeft;
                switch (alignment)
                {
                    case -1:
                        // Normal
                        Console.CursorLeft = 0;
                        break;
                    case 0:
                        // Centered
                        Console.CursorLeft = Convert.ToInt32((Console.WindowWidth / 2) - (text.Length / 2));
                        break;
                    case +1:
                        // Left
                        Console.CursorLeft = Convert.ToInt32((Console.WindowWidth / 2) - (text.Length / 2));
                        break;
                }
                RenderText(text);
                Console.CursorLeft = x0;
            }
            protected virtual void RenderClearRow(ConsoleColor background, uint offsetRY)
            {
                uint chars = (uint)Console.WindowWidth;
                uint x0 = (uint)Console.CursorLeft;
                uint y0 = (uint)Console.CursorTop;
                ConsoleColor bg0 = Console.BackgroundColor;
                Console.BackgroundColor = background;
                Console.CursorTop = (int)Math.Min(_clientY + offsetRY, Console.BufferHeight - 1);
                Console.CursorLeft = 0;
                Console.Write(Space((int)chars));
                Console.CursorTop = (int)y0;
                Console.CursorLeft = (int)x0;
                Console.BackgroundColor = bg0;
            }
            protected virtual void RenderClearArea(ConsoleColor background)
        {
            uint chars = (uint)(Console.BufferWidth * Console.WindowHeight);
            uint x0 = (uint)Console.CursorLeft;
            uint y0 = (uint)Console.CursorTop;
            ConsoleColor bg0 = Console.BackgroundColor;
            Console.BackgroundColor = background;
            Console.CursorTop = (int)_clientY;
            Console.CursorLeft = 0;
            Console.Write(Space((int)chars));
            Console.CursorTop = (int)y0;
            Console.CursorLeft = (int)x0;
            Console.BackgroundColor = bg0;
        }
        #endregion
        #region 7. Alias functions
            public void RenderBody()
            {
                RenderBody(_currentDocX, _currentDocY);
            }
            public void RenderClearArea(bool defaultCol)
            {
                if (defaultCol) { Console.ResetColor(); RenderClearArea(Console.BackgroundColor); }
                else
                    RenderClearArea(this.CurrentBodyBackground);
            }
        #endregion
    }
}